package edu.northwestern.cbits.purple_robot_manager.models; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.Iterator; import java.util.Map; import org.apache.commons.io.FileUtils; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.SharedPreferences; import android.content.res.AssetManager; import android.net.Uri; import edu.northwestern.cbits.purple_robot_manager.EncryptionManager; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.settings.SettingsActivity; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.models.trees.LeafNode; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; /** * Provides the infrastructure for fetching model definitions by URL and * generating dynamic model instances capable of evaluating incoming * information. */ public abstract class TrainedModel extends Model { protected Uri _source = null; protected String _sourceHash = null; protected boolean _inited = false; protected String _name = null; protected double _accuracy = 0.0; private long _lastCheck = 0; /** * Returns the URL of the model as the identifying URI. * * @see edu.northwestern.cbits.purple_robot_manager.models.Model#uri() */ @Override public Uri uri() { return this._source; } public TrainedModel(final Context context, Uri uri) { this._source = uri; this._sourceHash = EncryptionManager.getInstance().createHash(context, uri.toString(), "MD5"); final TrainedModel me = this; Runnable r = new Runnable() { @Override public void run() { String hash = EncryptionManager.getInstance().createHash(context, me._source.toString(), "MD5"); SharedPreferences prefs = Probe.getPreferences(context); File internalStorage = context.getFilesDir(); if (SettingsActivity.useExternalStorage(context)) internalStorage = context.getExternalFilesDir(null); if (internalStorage != null && !internalStorage.exists()) internalStorage.mkdirs(); File modelsFolder = new File(internalStorage, "persisted_models"); if (modelsFolder != null && !modelsFolder.exists()) modelsFolder.mkdirs(); String contents = null; File cachedModel = new File(modelsFolder, hash); try { contents = FileUtils.readFileToString(cachedModel); } catch (IOException e) { } try { BufferedReader in = null; if (me._source.toString().startsWith("file:///android_asset/")) { AssetManager assets = context.getAssets(); in = new BufferedReader(new InputStreamReader(assets.open(me._source.toString().replace( "file:///android_asset/", "")))); } else { URL u = new URL(me._source.toString()); in = new BufferedReader(new InputStreamReader(u.openStream())); } StringBuilder sb = new StringBuilder(); String inputLine = null; while ((inputLine = in.readLine()) != null) sb.append(inputLine); in.close(); contents = sb.toString(); } catch (IOException e) { e.printStackTrace(); LogManager.getInstance(context).logException(e); } if (contents != null) { try { JSONObject json = new JSONObject(contents); me._name = json.getString("name"); me._accuracy = json.getDouble("accuracy"); me.generateModel(context, json.get("model")); if (json.has("map")) me.setFeatureMap(context, json.getJSONObject("map")); FileUtils.writeStringToFile(cachedModel, contents); me._inited = true; } catch (Exception e) { LogManager.getInstance(context).logException(e); ModelManager.getInstance(context).deleteModel(me._source.toString()); } } } }; Thread t = new Thread(r); t.start(); } protected void setFeatureMap(Context context, JSONObject mapJson) { Iterator<String> keys = mapJson.keys(); while (keys.hasNext()) { try { String original = keys.next(); String replacement = mapJson.get(original).toString(); this._featureMap.put(original, replacement); } catch (JSONException e) { LogManager.getInstance(context).logException(e); } } } /** * Provides the name of the model, as specified by the "class" key in the * JSON definition. * * @see edu.northwestern.cbits.purple_robot_manager.models.Model#title(android.content.Context) */ @Override public String title(Context context) { return this._name; } /** * Provides a placeholder summary. TODO: Add feature to specify description * in JSON definition. * * @see edu.northwestern.cbits.purple_robot_manager.models.Model#summary(android.content.Context) */ @Override public String summary(Context context) { return context.getString(R.string.summary_model_unknown); } /** * Returns an MD5 hash of the model's URL to be used a unique identifier by * the rest of the system. * * @see edu.northwestern.cbits.purple_robot_manager.models.Model#getPreferenceKey() */ @Override public String getPreferenceKey() { return this._sourceHash; } /** * Returns the URL of the model definition to be used as the internal * identifier within. * * @see edu.northwestern.cbits.purple_robot_manager.models.Model#name(android.content.Context) */ @Override public String name(Context context) { return this._source.toString(); } /** * Calls TrainedModel.evaluateModel method within a Thread on implementing * subclasses to generate a prediction for the provided snapshot. When a * prediction becomes available, transmits the prediction through the rest * of the data processing pipeline. * * @see edu.northwestern.cbits.purple_robot_manager.models.Model#predict(android.content.Context, * java.util.Map) */ @Override public void predict(final Context context, final Map<String, Object> snapshot) { if (this._inited == false || this.enabled(context) == false) return; long now = System.currentTimeMillis(); if (now - this._lastCheck < 1000) { return; } this._lastCheck = now; for (String key : this._featureMap.keySet()) { String newKey = this._featureMap.get(key); if (snapshot.get(key) == null) { // Do nothing. } else { snapshot.put(newKey, snapshot.get(key)); } } final TrainedModel me = this; Runnable r = new Runnable() { @Override @SuppressWarnings("unchecked") public void run() { Object value = me.evaluateModel(context, snapshot); if (value == null) { // Do nothing. } else if (value instanceof Map<?, ?>) { Map<String, Object> map = (Map<String, Object>) value; if (map.get(LeafNode.PREDICTION) != null) me.transmitPrediction(context, map.get(LeafNode.PREDICTION).toString(), (Double) map.get(LeafNode.ACCURACY), map); } else if (value instanceof Double) { Double doubleValue = (Double) value; me.transmitPrediction(context, doubleValue, me._accuracy); } else me.transmitPrediction(context, value.toString(), me._accuracy); } }; Thread t = new Thread(r); t.start(); } /** * Generates the dynamic structures needed to evaluate the model provided. * Subclasses will implement this method to provide custom parsing logic * depending upon the representation provided. * * @param context * * @param model * Java object obtained from JSONObject.get that encodes the * desired model. * @throws Exception */ protected abstract void generateModel(Context context, Object model) throws Exception; /** * Evaluates the states provided to generate a prediction describing the * world at that point in time. This method will evaluate the input provided * against the data structure constructed in the generateModel method. * * @param context * @param snapshot * Key-value pairs describing the states to be evaluated. * * @return Prediction generated by the model. */ protected abstract Object evaluateModel(Context context, Map<String, Object> snapshot); }